package pl.eurekin;

import com.sun.tools.javac.tree.JCTree.JCLiteral;
import static lombok.javac.handlers.JavacHandlerUtil.*;


import lombok.AccessLevel;
import lombok.core.AST.Kind;
import lombok.core.handlers.TransformationsUtil;
import lombok.javac.Javac;
import lombok.javac.JavacNode;


import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCTypeParameter;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCNewClass;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
import lombok.Setter;
import lombok.javac.handlers.HandleData;

/**
 *
 * @author Rekin
 */
public class HandlePropertySetter {

    public boolean generateSetterForType(JavacNode typeNode, JavacNode errorNode, AccessLevel level, boolean checkForTypeLevelSetter) {
        if (checkForTypeLevelSetter) {
            if (typeNode != null) {
                for (JavacNode child : typeNode.down()) {
                    if (child.getKind() == Kind.ANNOTATION) {
                        if (Javac.annotationTypeMatches(Setter.class, child)) {
                            //The annotation will make it happen, so we can skip it.
                            return true;
                        }
                    }
                }
            }
        }

        JCClassDecl typeDecl = null;
        if (typeNode.get() instanceof JCClassDecl) {
            typeDecl = (JCClassDecl) typeNode.get();
        }
        long modifiers = typeDecl == null ? 0 : typeDecl.mods.flags;
        boolean notAClass = (modifiers & (Flags.INTERFACE | Flags.ANNOTATION | Flags.ENUM)) != 0;

        if (typeDecl == null || notAClass) {
            errorNode.addError("@Setter is only supported on a class or a field.");
            return false;
        }

        for (JavacNode field : typeNode.down()) {
            if (field.getKind() != Kind.FIELD) {
                continue;
            }
            JCVariableDecl fieldDecl = (JCVariableDecl) field.get();
            //Skip fields that start with $
            if (fieldDecl.name.toString().startsWith("$")) {
                continue;
            }
            //Skip static fields.
            if ((fieldDecl.mods.flags & Flags.STATIC) != 0) {
                continue;
            }
            //Skip final fields.
            if ((fieldDecl.mods.flags & Flags.FINAL) != 0) {
                continue;
            }

            generateSetterForField(field, errorNode.get(), level);
        }
        return true;
    }

    /**
     * Generates a setter on the stated field.
     *
     * Used by {@link HandleData}.
     *
     * The difference between this call and the handle method is as follows:
     *
     * If there is a {@code lombok.Setter} annotation on the field, it is used and the
     * same rules apply (e.g. warning if the method already exists, stated access level applies).
     * If not, the setter is still generated if it isn't already there, though there will not
     * be a warning if its already there. The default access level is used.
     *
     * @param fieldNode The node representing the field you want a setter for.
     * @param pos The node responsible for generating the setter (the {@code @Data} or {@code @Setter} annotation).
     */
    public void generateSetterForField(JavacNode fieldNode, DiagnosticPosition pos, AccessLevel level) {
        for (JavacNode child : fieldNode.down()) {
            if (child.getKind() == Kind.ANNOTATION) {
                if (Javac.annotationTypeMatches(Setter.class, child)) {
                    //The annotation will make it happen, so we can skip it.
                    return;
                }
            }
        }

        createSetterForField(level, fieldNode, fieldNode, false);
    }

    private boolean createSetterForField(AccessLevel level,
            JavacNode fieldNode, JavacNode errorNode, boolean whineIfExists) {
        if (fieldNode.getKind() != Kind.FIELD) {
            fieldNode.addError("@Setter is only supported on a class or a field.");
            return true;
        }

        JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get();
        String methodName = toSetterName(fieldDecl);

        switch (methodExists(methodName, fieldNode, false)) {
            case EXISTS_BY_LOMBOK:
                return true;
            case EXISTS_BY_USER:
                if (whineIfExists) {
                    errorNode.addWarning(
                            String.format("Not generating %s(%s %s): A method with that name already exists",
                            methodName, fieldDecl.vartype, fieldDecl.name));
                }
                return true;
            default:
            case NOT_EXISTS:
            //continue with creating the setter
        }

        long access = toJavacModifier(level) | (fieldDecl.mods.flags & Flags.STATIC);

        injectMethod(fieldNode.up(), createSetter(access, fieldNode, fieldNode.getTreeMaker()));
        injectField(fieldNode.up(), createPropertyStringField(fieldNode, fieldNode.getTreeMaker()));

        return true;
    }

    /**
    <FIELD String PROP_PROPERTY>
    <class com.sun.tools.javac.tree.JCTree$JCLiteral>
    "property"
    </class com.sun.tools.javac.tree.JCTree$JCLiteral>
    </FIELD String PROP_PROPERTY>
     * @param field
     * @param treeMaker
     */
    private JCVariableDecl createPropertyStringField(JavacNode typeNode, TreeMaker maker) {
        JCVariableDecl newVar = maker.VarDef(
                maker.Modifiers(Flags.PUBLIC | Flags.STATIC | Flags.FINAL),
                typeNode.toName(getNameForPropertyString(typeNode)),
                chainDots(maker, typeNode, "java", "lang", "String"),
                getValueForPropertyString(maker, typeNode) //newClass - WTF, why can't I haz normal init here
                );
        return newVar;
    }

    private JCLiteral getValueForPropertyString(TreeMaker maker, JavacNode typeNode) {
        return maker.Literal(typeNode.getName().toString());
    }

    private String getNameForPropertyString(JavacNode typeNode) {
        String prefix = "PROP_";
        String infix = typeNode.getName().toString().toUpperCase();
        return prefix + infix;
    }

    private JCMethodDecl createSetter(long access, JavacNode field, TreeMaker treeMaker) {
        JCVariableDecl fieldDecl = (JCVariableDecl) field.get();

        JCFieldAccess thisX = treeMaker.Select(treeMaker.Ident(field.toName("this")), fieldDecl.name);
        JCAssign assign = treeMaker.Assign(thisX, treeMaker.Ident(fieldDecl.name));

        List<JCStatement> statements;
        List<JCAnnotation> nonNulls = findAnnotations(field, TransformationsUtil.NON_NULL_PATTERN);
        List<JCAnnotation> nullables = findAnnotations(field, TransformationsUtil.NULLABLE_PATTERN);
        // My code
        JCFieldAccess pCh = treeMaker.Select(treeMaker.Ident(field.toName("$propertyChangeSupport")), field.toName("firePropertyChange"));
        JCMethodInvocation firePChSupp = treeMaker.Apply(
                List.<JCExpression>nil(),
                pCh,
                List.<JCExpression>of(
                treeMaker.Literal(TypeTags.CLASS, fieldDecl.name.toString()), // "field"
                thisX, // this.field
                assign // this.field = field
                ));



        JCExpression fieldEqualsNull = treeMaker.Binary(JCTree.EQ, treeMaker.Ident(field.toName("$propertyChangeSupport")), treeMaker.Literal(TypeTags.BOT, null));
        JCExpression constructedType = chainDots(treeMaker, field, "java", "beans", "PropertyChangeSupport");
        JCIdent thisLiteral = treeMaker.Ident(field.toName("this"));
        ListBuffer<JCExpression> myArgs = new ListBuffer<JCExpression>();
        myArgs.append(thisLiteral);
        JCNewClass newPCh = treeMaker.NewClass(null, List.<JCExpression>nil(), constructedType, myArgs.toList(), null);
        JCStatement declareField = treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(field.toName("$propertyChangeSupport")), newPCh));
        JCStatement unNull = treeMaker.If(fieldEqualsNull, declareField, null);

        // </mycode>
        if (nonNulls.isEmpty()) {
            statements = List.<JCStatement>of(unNull,
                    treeMaker.Exec(firePChSupp));
        } else {
            JCStatement nullCheck = generateNullCheck(treeMaker, field);
            if (nullCheck != null) {
                statements = List.<JCStatement>of(nullCheck, unNull, treeMaker.Exec(firePChSupp));
            } else {
                statements = List.<JCStatement>of(unNull, treeMaker.Exec(firePChSupp));
            }
        }

        JCBlock methodBody = treeMaker.Block(0, statements);
        Name methodName = field.toName(toSetterName(fieldDecl));
        JCVariableDecl param = treeMaker.VarDef(
                treeMaker.Modifiers(Flags.FINAL, nonNulls.appendList(nullables)),
                fieldDecl.name, fieldDecl.vartype, null);
        //WARNING: Do not use field.getSymbolTable().voidType - that field has gone through non-backwards compatible API changes within javac1.6.
        JCExpression methodType = treeMaker.Type(new Type(TypeTags.VOID, null));
        List<JCTypeParameter> methodGenericParams = List.nil();
        List<JCVariableDecl> parameters = List.of(param);
        List<JCExpression> throwsClauses = List.nil();
        JCExpression annotationMethodDefaultValue = null;

        return treeMaker.MethodDef(
                treeMaker.Modifiers(access, List.<JCAnnotation>nil()),
                methodName,
                methodType,
                methodGenericParams,
                parameters,
                throwsClauses,
                methodBody,
                annotationMethodDefaultValue);
    }
}
